////////////////////////////////////////////////////////////////////////////////
//
//  FLEX USER GROUP HOKURIKU
//  Copyright 2009 Flex User Group Hokuriku
//  All Rights Reserved.
// 
//  NOTICE: Flex User Group Hokuriku permits you to use, 
//  modify, and distribute this file 
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package net.fxug.hokuriku.chocolat.dao
{
    import flash.data.SQLConnection;
    import flash.data.SQLResult;
    import flash.data.SQLStatement;
    import flash.errors.IllegalOperationError;
    import flash.events.EventDispatcher;
    import flash.events.SQLErrorEvent;
    import flash.events.SQLEvent;
    import flash.filesystem.File;
    
    import mx.logging.ILogger;
    import mx.logging.Log;
    
    import net.fxug.hokuriku.chocolat.dao.contents.SQLQuery;
    import net.fxug.hokuriku.chocolat.errors.AlreadyConnectedError;
    import net.fxug.hokuriku.chocolat.errors.WarningError;
    import net.fxug.hokuriku.chocolat.events.SQLVoteEvent;
    import net.fxug.hokuriku.chocolat.utils.DatetimeUtil;
    import net.fxug.hokuriku.chocolat.utils.LoggingUtil;

    /**
     * セッション登録イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.INSERT_SESSION_EVENT
     */
    [Event(name="insertSessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * セッション取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_SESSION_EVENT
     */
    [Event(name="selectSessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * 投票登録イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.INSERT_VOTE_EVENT
     */
    [Event(name="insertVoteEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * 投票取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_VOTE_EVENT
     */
    [Event(name="selectVoteEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * セッションID指定時の投票情報取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_VOTE_BY_SESSIONID_EVENT
     */
    [Event(name="selectVoteBySessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * DB接続イベント
     * @eventType flash.events.SQLEvent.OPEN
     */
    [Event(name="open", type="flash.events.SQLEvent")]
    /**
     * 取得失敗イベント
     * @eventType flash.events.SQLErrorEvent.ERROR
     */
    [Event(name="error", type="flash.events.SQLErrorEvent")]

    /**
     * データベースへの接続を管理するためのマネージャークラス.
     * このクラスはシングルトンで動作する。
	 *
     * このクラスで実行されるDB関連処理は非同期で実行される。
     *
     * TODO 値の検証が実装されていない。
     *
     * @author hikaruworld
     */
    public class DatabaseManager extends EventDispatcher
    {
        /** 自身を表す一意なインスタンス */
        private static var instance:DatabaseManager;

        /** ロガー */
        private static var logger:ILogger = LoggingUtil.getLogger(DatabaseManager);

        /**
         * SQL接続を行うSQLConnection
         * TODO 必要に応じてこのSQLConnectionをキュー化してもよい
         */
        private var conn:SQLConnection;

        /** セッション内容取得に利用されるSQLStatement */
        private var findSessionStmt:SQLStatement;
        /** セッション内容登録に利用されるSQLStatement */
        private var insertSessionStmt:SQLStatement;
        /** 投票内容取得に利用されるSQLStatement */
        private var findVoteStmt:SQLStatement;
        /** 投票内容登録に利用されるSQLStatement */
        private var insertVoteStmt:SQLStatement;
        /** セッションIDを指定した投票内容取得のためのSQLStatement */
        private var findVoteBySessionIdStmt:SQLStatement;

        /**
         * 自身の一意なインスタンスを取得する。
         */
        public static function getInstance():DatabaseManager
        {
            if (instance == null)
            {
                instance = new DatabaseManager(new SingletonBlockr());
            }
            return instance;
        }

		/**
         * コンストラクタ
		 * @param blockr Singleton制御用
		 */		
        public function DatabaseManager(blockr:SingletonBlockr = null)
        {
            if (blockr == null)
            {
                throw new IllegalOperationError("このクラスの外部からのインスタンス化は許可されていません");
            }
            this.init();
        }

        /**
         * 現在の接続状態を返す。
         * @return 接続されいる状態であればtrue
         */
        public function isCoonected():Boolean
        {
            return this.conn == null ? false : this.conn.connected;
        }

		/**
         * セッション情報を登録する。
		 * @param sessionDao net.fxug.hokuriku.chocolat.dao.SessionDao
		 */		
        public function insertSession(sessionDao:SessionDao):void
        {
            this.insertSessionStmt.clearParameters();
//			this.insertSessionStmt.parameters["sessionId"] = sessionDao.sessionId;
            this.insertSessionStmt.parameters[":startDatetime"] = DatetimeUtil.roundDownByMillsecond(sessionDao.startDatetime);
            this.insertSessionStmt.parameters[":sessionTag"] = sessionDao.sessionTag;
            this.insertSessionStmt.parameters[":title"] = sessionDao.title;
            this.insertSessionStmt.parameters[":atndId"] = sessionDao.atndId;

            logger.debug("sql::{0}[param::{1}]", this.insertSessionStmt.text, sessionDao);
            this.insertSessionStmt.execute();
        }

        /**
         * セッション情報を取得する。
         */
        public function selectSession():void
        {
            this.findSessionStmt.clearParameters();
            this.findSessionStmt.itemClass = SessionDao;

            logger.debug("sql::{0}[param::{1}]", findSessionStmt.text);
        }

        /**
         * 投票情報を登録する。
         */
        public function insertVote(voteDao:VoteDao):void
        {
            this.insertVoteStmt.clearParameters();
            this.insertVoteStmt.parameters[":datetime"] = DatetimeUtil.roundDownByMillsecond(voteDao.datetime);
            this.insertVoteStmt.parameters[":protocol"] = voteDao.protocol;
            this.insertVoteStmt.parameters[":sessionId"] = voteDao.sessionId;
            this.insertVoteStmt.parameters[":tweet"] = voteDao.tweet;
            this.insertVoteStmt.parameters[":userId"] = voteDao.userId;
            this.insertVoteStmt.parameters[":route"] = voteDao.route;

            logger.debug("sql::{0}[param::{1}]", this.insertVoteStmt.text, voteDao);
            this.insertVoteStmt.execute();
        }

		/**
         * 投票情報を登録する。
         * 複数のVote情報を登録する際に利用する。
         * 注意点:このメソッドはSQLStatementを実行時のタイミングで生成するので効率が悪い。
		 * 
		 * @param voteArray net.fxug.hokuriku.chocolat.dao.VoteDaoの配列
		 */		
        public function insertMultiVote(voteArray:Array):void
        {
            if (voteArray == null || voteArray.length == 0)
            {
                return;
            }
            logger.debug("insertMultiVote::{0} data", voteArray.length);
            var now:int = 0;
            var limit:int = voteArray.length;
            var multiInsertVoteStmt:SQLStatement = new SQLStatement();

            multiInsertVoteStmt.sqlConnection = this.conn;
            multiInsertVoteStmt.text = SQLQuery.INSERT_VOTE;
            var errorFunction:Function = function(sqlError:SQLErrorEvent):void
                {
                    multiInsertVoteStmt.removeEventListener(SQLErrorEvent.ERROR, errorFunction);
                    logger.error("errorID::{0}	text::{1}", sqlError.errorID, sqlError.text);
                    dispatchEvent(sqlError);
                }
            multiInsertVoteStmt.addEventListener(SQLErrorEvent.ERROR, errorFunction);
            var resultFunction:Function = function(sqlEvent:SQLEvent):void
                {
                    multiInsertVoteStmt.removeEventListener(SQLEvent.RESULT, resultFunction);
                    var result:SQLResult = multiInsertVoteStmt.getResult();
                    trace(result.data);
                    now++;
                    if (now < limit)
                    {
                        logger.debug("insertMultiVote::{0} / {1}", now, voteArray.length);
                        internalInsert(multiInsertVoteStmt, now, voteArray);
                    }
                }
            multiInsertVoteStmt.addEventListener(SQLEvent.RESULT, resultFunction);
            this.internalInsert(multiInsertVoteStmt, now, voteArray);
        }
		/**
		 * Vote登録処理を行う。
		 * @param multiInsertVoteStmt 登録処理に用いられるSQLStatement
		 * @param now 現在のindex
		 * @param voteArray net.fxug.hokuriku.chocolat.dao.VoteDaoの配列
		 */		
        private function internalInsert(multiInsertVoteStmt:SQLStatement, now:int, voteArray:Array):void
        {
            var voteDao:VoteDao = voteArray[now];
            multiInsertVoteStmt.clearParameters();
            multiInsertVoteStmt.parameters[":datetime"] = DatetimeUtil.roundDownByMillsecond(voteDao.datetime);
            multiInsertVoteStmt.parameters[":protocol"] = voteDao.protocol;
            multiInsertVoteStmt.parameters[":sessionId"] = voteDao.sessionId;
            multiInsertVoteStmt.parameters[":tweet"] = voteDao.tweet;
            multiInsertVoteStmt.parameters[":userId"] = voteDao.userId;
            multiInsertVoteStmt.parameters[":route"] = voteDao.route;

            logger.debug("sql::{0}[param::{1}]", multiInsertVoteStmt.text, voteDao);
            multiInsertVoteStmt.execute();
        }

        /**
         * 投票情報を取得する
         */
        public function selectVote():void
        {
            this.findVoteStmt.clearParameters();
            this.findVoteStmt.itemClass = VoteDao;

            logger.debug("sql::{0}[param::{1}]", this.findVoteStmt.text);
            this.findVoteStmt.execute();
        }

		/**
         * セッションIDを指定して投票情報を取得する。
		 * @param sessionId セッションID
		 */		
        public function findVoteBySessionId(sessionId:int):void
        {
            if (sessionId <= 0)
            {
                throw new ArgumentError("sessionIdに不正な値::" + String(sessionId) + "::が指定されました");
            }
            this.findVoteBySessionIdStmt.clearParameters();
            this.findVoteBySessionIdStmt.parameters[":sessionId"] = sessionId;

            logger.debug("sql::{0}[param::{1}]", this.findVoteBySessionIdStmt.text, sessionId);
            this.findVoteBySessionIdStmt.execute();
        }

        /**
         * 生成したSQLConnectionを利用してDBへ接続を行う。
         * @param trueの場合に既存のDBを削除して新規作成する。
         */
        public function connect(init:Boolean = false):void
        {
            // 既に接続されている場合はエラーを返す。
            if (this.conn != null && this.conn.connected)
            {
                throw new AlreadyConnectedError("対象DBには既に接続しています");
            }
            // DBの存在チェックを行い、存在しなかった場合はローカルテンプレートからコピーする
            var dbFile:File = File.applicationStorageDirectory.resolvePath("soraai.db");
            logger.debug("dbFile.exists : " + dbFile.exists);
            logger.debug("dbFile.netivePath : " + dbFile.nativePath);

            // 初期化が必要でかつ、既存のDBが存在しない場合
            if (init)
            {
                if (dbFile.exists)
                {
                    dbFile.deleteFile();
                }
                this.createDatabase(dbFile);
            }
            // 初期化が不要の場合
            else
            {
                if (!dbFile.exists)
                {
					logger.warn("初期化しない設定にも関わらず既存DBが存在しなかっため、新規作成を行います。");
                    this.createDatabase(dbFile);
                }
                else
                {
                    //TODO 検証が問題ない場合と問題あった場合で処理を分岐
                    var verified:Boolean = this.verifyDatabase(dbFile);
                    logger.debug("verified::=" + verified);
                }
            }
            // 非同期モードで接続
            this.conn.openAsync(dbFile);
        }

		/**
         * DBに非同期モードで正常に接続できた場合に発行される。
		 * @param sqlEvent DB接続時に発行されるSQLEvent
		 */		
        private function conn_openHandler(sqlEvent:SQLEvent):void
        {
            this.conn.removeEventListener(SQLEvent.OPEN, conn_openHandler);
            this.conn.removeEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
            logger.debug("DBとのコネクションが確立されました。");

            // Statementの初期化を行う。
            // セッション系
            logger.debug("Create Statements SELECT SESSION...");
            this.findSessionStmt = new SQLStatement();
            this.findSessionStmt.sqlConnection = this.conn;
            this.findSessionStmt.text = SQLQuery.SELECT_SESSION;
            this.findSessionStmt.itemClass = SessionDao;
            this.findSessionStmt.addEventListener(SQLEvent.RESULT, findSessionStmt_resultHandler);
            this.findSessionStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
            logger.debug("Create Statements INSERT SESSION...");
            this.insertSessionStmt = new SQLStatement();
            this.insertSessionStmt.sqlConnection = this.conn;
            this.insertSessionStmt.text = SQLQuery.INSERT_SESSION;
            this.insertSessionStmt.addEventListener(SQLEvent.RESULT, insertSessionStmt_resultHandler);
            this.insertSessionStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            // 投票系
            logger.debug("Create Statements SELECT VOTE...");
            this.findVoteStmt = new SQLStatement();
            this.findVoteStmt.sqlConnection = this.conn;
            this.findVoteStmt.text = SQLQuery.SELECT_VOTE;
            this.findVoteStmt.itemClass = VoteDao;
            this.findVoteStmt.addEventListener(SQLEvent.RESULT, findVoteStmt_resultHandler);
            this.findVoteStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            logger.debug("Create Statements INSERT VOTE...");
            this.insertVoteStmt = new SQLStatement();
            this.insertVoteStmt.sqlConnection = this.conn;
            this.insertVoteStmt.text = SQLQuery.INSERT_VOTE;
            this.insertVoteStmt.addEventListener(SQLEvent.RESULT, insertVoteStmt_resultHandler);
            this.insertVoteStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            logger.debug("Create Statements SELECT VOTE BY SESSION_ID...");
            this.findVoteBySessionIdStmt = new SQLStatement();
            this.findVoteBySessionIdStmt.sqlConnection = this.conn;
            this.findVoteBySessionIdStmt.text = SQLQuery.SELECT_VOTE_BY_SESSION_ID;
            this.findVoteBySessionIdStmt.itemClass = VoteDao;
            this.findVoteBySessionIdStmt.addEventListener(SQLEvent.RESULT, findVoteBySessionIdStmt_resultHandler);
            this.findVoteBySessionIdStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            this.dispatchEvent(sqlEvent);
        }

		/**
         * DBを作成する。
		 * @param toFile 生成先ファイル
		 */		
        private function createDatabase(toFile:File):void
        {
            // 削除後新規作成する。
            //Application内部からテンプレートを取得
            var tempFile:File = File.applicationDirectory.resolvePath("assets/db/soraai.db");
            // 添付ファイルがもし存在しない場合はSQLを流して接続する。
            if (!tempFile.exists)
            {
                logger.info("テンプレートファイルが参照できなかったため、CREATE文を使ってテーブルを作成します。");
                throw new IllegalOperationError("実装先送り");
            }
            // 同期モードで上書きコピーする。
            try
            {
                tempFile.copyTo(toFile, true);
				logger.debug("同期モードでを用いてテンプレートからのファイルコピーが完了しました.");
            }
            catch (error:Error)
            {
				throw new WarningError(error.message, error.errorID);
            }
        }

		/**
         * DBのテーブル構成を検証する。
		 * @param file 検証対象DBファイル
         * @reuturn DBが問題ない場合にtrueを返す。
		 */		
        private function verifyDatabase(file:File):Boolean
        {
            //TODO 検証ロジックの実装。DBバージョンのチェック
            //FIXME とりあえずファイルが存在すればtrueを返す。
            return file != null ? file.exists : false;
        }

        /**
         * DB接続を終了する。
         */
        public function destroy():void
        {
            if (this.conn == null || !this.conn.connected)
            {
                logger.info("既にDB接続がCloseされているか、インスタンスが存在していないため処理しません.");
                return;
            }
            this.conn.addEventListener(SQLEvent.CLOSE, conn_closeEventHandler);
            this.conn.close();
        }

		/**
		 * SQLのCLOSE完了時に発行される。
		 * @param event SQLEvent
		 */		
        private function conn_closeEventHandler(event:SQLEvent):void
        {
            this.conn.removeEventListener(SQLEvent.CLOSE, conn_closeEventHandler);
            logger.info("DBを正常にcloseしました.");
        }

		/**
         * 初期化処理.
         * 各種SQLのQueryの接続及び初期化を行う.
		 */		
        private function init():void
        {
            logger = Log.getLogger("net.fxug.hokuriku.chocolat.dao.DatabaseManager");
            logger.info("initlized DatabaseController.....");

            this.conn = new SQLConnection();
            this.conn.addEventListener(SQLEvent.OPEN, conn_openHandler);
            this.conn.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
        }
		/**
         * セッション情報登録成功時のイベントハンドラ
		 * @param resultEvent flash.events.SQLEvent
		 */		
        private function insertSessionStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.insertSessionStmt.getResult();

            logger.info("insertSessionStmt_resultHandler::" + sqlResult);

            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.INSERT_SESSION_EVENT, sqlResult.lastInsertRowID, resultEvent.bubbles, resultEvent.cancelable);
            this.dispatchEvent(retEvent);
        }

		/**
         * セッション情報取得成功時のイベントハンドラ
		 * @param resultEvent flash.events.SQLEvent
		 */		
        private function findSessionStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.findSessionStmt.getResult();
            logger.info("findSessionStmt_resultHandler::" + resultEvent);
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_SESSION_EVENT, this.findSessionStmt.getResult().data, resultEvent.bubbles, resultEvent.cancelable);
            this.dispatchEvent(retEvent);
        }

		/**
         * 投票登録成功時のイベントハンドラ
		 * @param resultEvent flash.events.SQLEvent
		 */		
        private function insertVoteStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.insertVoteStmt.getResult();
            logger.info("insertVoteStmt_resultHandler::" + sqlResult);
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.INSERT_VOTE_EVENT, sqlResult.lastInsertRowID, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

		/**
         * 投票データ取得成功時のイベントハンドラ
		 * @param resultEvent flash.events.SQLEvent
		 */		
        private function findVoteStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.findVoteStmt.getResult();
            logger.debug("findVoteStmt_resultHandler...");
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_VOTE_EVENT, sqlResult.data, resultEvent.bubbles, resultEvent.cancelable);
            this.dispatchEvent(retEvent);
        }

		/**
		 * セッション情報取得完了時のイベントハンドラ
		 * @param resultEvent flash.events.SQLEvent
		 */		
        private function findVoteBySessionIdStmt_resultHandler(resultEvent:SQLEvent):void
        {
            logger.debug("findVoteBySessionIdStmt_resultHandler...");
            var result:SQLResult = this.findVoteBySessionIdStmt.getResult();
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_VOTE_BY_SESSIONID_EVENT, result.data, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

		/**
         * SQLのOpenに失敗した場合に表示されるイベントハンドラ
		 * @param sqlErrorEvent flash.events.SQLErrorEvent
		 */		
        private function sqlErrorHandler(sqlErrorEvent:SQLErrorEvent):void
        {
            logger.error("Error message:", sqlErrorEvent.error.message, " Details:", sqlErrorEvent.error.details);
            this.dispatchEvent(sqlErrorEvent);
        }
    }
}

/** インスタンス生成抑止用 */
class SingletonBlockr
{
}
